<?php

namespace Aws\S3;

use Aws\Api\Parser\PayloadParserTrait;
use Aws\CommandInterface;
use Aws\Exception\AwsException;
use Aws\HandlerList;
use Aws\ResultInterface;
use Aws\S3\Exception\PermanentRedirectException;
use Aws\S3\Exception\S3Exception;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use Psr\Http\Message\ResponseInterface;

/**
 * A trait providing S3-specific functionality. This is meant to be used in
 * classes implementing \Aws\S3\S3ClientInterface
 */
trait S3ClientTrait {
	use PayloadParserTrait;

	/**
	 * @see S3ClientInterface::upload()
	 */
	public function upload(
		$bucket,
		$key,
		$body,
		$acl = 'private',
		array $options = []
	) {
		return $this
			->uploadAsync($bucket, $key, $body, $acl, $options)
			->wait();
	}

	/**
	 * @see S3ClientInterface::uploadAsync()
	 */
	public function uploadAsync(
		$bucket,
		$key,
		$body,
		$acl = 'private',
		array $options = []
	) {
		return (new ObjectUploader($this, $bucket, $key, $body, $acl, $options))
			->promise();
	}

	/**
	 * @see S3ClientInterface::copy()
	 */
	public function copy(
		$fromB,
		$fromK,
		$destB,
		$destK,
		$acl = 'private',
		array $opts = []
	) {
		return $this->copyAsync($fromB, $fromK, $destB, $destK, $acl, $opts)
			->wait();
	}

	/**
	 * @see S3ClientInterface::copyAsync()
	 */
	public function copyAsync(
		$fromB,
		$fromK,
		$destB,
		$destK,
		$acl = 'private',
		array $opts = []
	) {
		$source = [
			'Bucket' => $fromB,
			'Key' => $fromK,
		];
		if(isset($opts['version_id'])) {
			$source['VersionId'] = $opts['version_id'];
		}
		$destination = [
			'Bucket' => $destB,
			'Key' => $destK
		];

		return (new ObjectCopier($this, $source, $destination, $acl, $opts))
			->promise();
	}

	/**
	 * @see S3ClientInterface::registerStreamWrapper()
	 */
	public function registerStreamWrapper() {
		StreamWrapper::register($this);
	}

	/**
	 * @see S3ClientInterface::registerStreamWrapperV2()
	 */
	public function registerStreamWrapperV2() {
		StreamWrapper::register(
			$this,
			's3',
			null,
			true
		);
	}

	/**
	 * @see S3ClientInterface::deleteMatchingObjects()
	 */
	public function deleteMatchingObjects(
		$bucket,
		$prefix = '',
		$regex = '',
		array $options = []
	) {
		$this->deleteMatchingObjectsAsync($bucket, $prefix, $regex, $options)
			->wait();
	}

	/**
	 * @see S3ClientInterface::deleteMatchingObjectsAsync()
	 */
	public function deleteMatchingObjectsAsync(
		$bucket,
		$prefix = '',
		$regex = '',
		array $options = []
	) {
		if(!$prefix && !$regex) {
			return new RejectedPromise(
				new \RuntimeException('A prefix or regex is required.')
			);
		}

		$params = ['Bucket' => $bucket, 'Prefix' => $prefix];
		$iter = $this->getIterator('ListObjects', $params);

		if($regex) {
			$iter = \Aws\filter($iter, function($c) use ($regex) {
				return preg_match($regex, $c['Key']);
			});
		}

		return BatchDelete::fromIterator($this, $bucket, $iter, $options)
			->promise();
	}

	/**
	 * @see S3ClientInterface::uploadDirectory()
	 */
	public function uploadDirectory(
		$directory,
		$bucket,
		$keyPrefix = null,
		array $options = []
	) {
		$this->uploadDirectoryAsync($directory, $bucket, $keyPrefix, $options)
			->wait();
	}

	/**
	 * @see S3ClientInterface::uploadDirectoryAsync()
	 */
	public function uploadDirectoryAsync(
		$directory,
		$bucket,
		$keyPrefix = null,
		array $options = []
	) {
		$d = "s3://$bucket".($keyPrefix ? '/'.ltrim($keyPrefix, '/') : '');
		return (new Transfer($this, $directory, $d, $options))->promise();
	}

	/**
	 * @see S3ClientInterface::downloadBucket()
	 */
	public function downloadBucket(
		$directory,
		$bucket,
		$keyPrefix = '',
		array $options = []
	) {
		$this->downloadBucketAsync($directory, $bucket, $keyPrefix, $options)
			->wait();
	}

	/**
	 * @see S3ClientInterface::downloadBucketAsync()
	 */
	public function downloadBucketAsync(
		$directory,
		$bucket,
		$keyPrefix = '',
		array $options = []
	) {
		$s = "s3://$bucket".($keyPrefix ? '/'.ltrim($keyPrefix, '/') : '');
		return (new Transfer($this, $s, $directory, $options))->promise();
	}

	/**
	 * @see S3ClientInterface::determineBucketRegion()
	 */
	public function determineBucketRegion($bucketName) {
		return $this->determineBucketRegionAsync($bucketName)->wait();
	}

	/**
	 * @param string $bucketName
	 *
	 * @return PromiseInterface
	 * @see S3ClientInterface::determineBucketRegionAsync()
	 *
	 */
	public function determineBucketRegionAsync($bucketName) {
		$command = $this->getCommand('HeadBucket', ['Bucket' => $bucketName]);
		$handlerList = clone $this->getHandlerList();
		$handlerList->remove('s3.permanent_redirect');
		$handlerList->remove('signer');
		$handler = $handlerList->resolve();

		return $handler($command)
			->then(static function(ResultInterface $result) {
				return $result['@metadata']['headers']['x-amz-bucket-region'];
			}, function(AwsException $e) {
				$response = $e->getResponse();
				if($response === null) {
					throw $e;
				}

				if($e->getAwsErrorCode() === 'AuthorizationHeaderMalformed') {
					$region = $this->determineBucketRegionFromExceptionBody(
						$response
					);
					if(!empty($region)) {
						return $region;
					}
					throw $e;
				}

				return $response->getHeaderLine('x-amz-bucket-region');
			});
	}

	private function determineBucketRegionFromExceptionBody(ResponseInterface $response) {
		try {
			$element = $this->parseXml($response->getBody(), $response);
			if(!empty($element->Region)) {
				return (string)$element->Region;
			}
		} catch (\Exception $e) {
			// Fallthrough on exceptions from parsing
		}
		return false;
	}

	/**
	 * @see S3ClientInterface::doesBucketExist()
	 */
	public function doesBucketExist($bucket) {
		return $this->checkExistenceWithCommand(
			$this->getCommand('HeadBucket', ['Bucket' => $bucket])
		);
	}

	/**
	 * @see S3ClientInterface::doesBucketExistV2()
	 */
	public function doesBucketExistV2($bucket, $accept403 = false) {
		$command = $this->getCommand('HeadBucket', ['Bucket' => $bucket]);

		try {
			$this->execute($command);
			return true;
		} catch (S3Exception $e) {
			if(
				($accept403 && $e->getStatusCode() === 403)
				|| $e instanceof PermanentRedirectException
			) {
				return true;
			}
			if($e->getStatusCode() === 404) {
				return false;
			}
			throw $e;
		}
	}

	/**
	 * @see S3ClientInterface::doesObjectExist()
	 */
	public function doesObjectExist($bucket, $key, array $options = []) {
		return $this->checkExistenceWithCommand(
			$this->getCommand('HeadObject', [
					'Bucket' => $bucket,
					'Key' => $key
				] + $options)
		);
	}

	/**
	 * @see S3ClientInterface::doesObjectExistV2()
	 */
	public function doesObjectExistV2(
		$bucket,
		$key,
		$includeDeleteMarkers = false,
		array $options = []
	) {
		$command = $this->getCommand('HeadObject', [
				'Bucket' => $bucket,
				'Key' => $key
			] + $options
		);

		try {
			$this->execute($command);
			return true;
		} catch (S3Exception $e) {
			if($includeDeleteMarkers
				&& $this->useDeleteMarkers($e)
			) {
				return true;
			}
			if($e->getStatusCode() === 404) {
				return false;
			}
			throw $e;
		}
	}

	private function useDeleteMarkers($exception) {
		$response = $exception->getResponse();
		return !empty($response)
			&& $response->getHeader('x-amz-delete-marker');
	}

	/**
	 * Determines whether or not a resource exists using a command
	 *
	 * @param CommandInterface $command Command used to poll for the resource
	 *
	 * @return bool
	 * @throws S3Exception|\Exception if there is an unhandled exception
	 */
	private function checkExistenceWithCommand(CommandInterface $command) {
		try {
			$this->execute($command);
			return true;
		} catch (S3Exception $e) {
			if($e->getAwsErrorCode() == 'AccessDenied') {
				return true;
			}
			if($e->getStatusCode() >= 500) {
				throw $e;
			}
			return false;
		}
	}

	/**
	 * @see S3ClientInterface::execute()
	 */
	abstract public function execute(CommandInterface $command);

	/**
	 * @see S3ClientInterface::getCommand()
	 */
	abstract public function getCommand($name, array $args = []);

	/**
	 * @return HandlerList
	 * @see S3ClientInterface::getHandlerList()
	 *
	 */
	abstract public function getHandlerList();

	/**
	 * @return \Iterator
	 * @see S3ClientInterface::getIterator()
	 *
	 */
	abstract public function getIterator($name, array $args = []);
}
